﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.Text;
using System.Threading;
using FinMkts.Messaging;

namespace FinMkts.Prices.ClientService
{
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
    public class PublishSubscribeService : IPublishSubscribe
    {
        static readonly TypedMessageConverter MessageConverter = TypedMessageConverter.Create(
            typeof(NotificationData),
            NotificationData.NotificationAction,
            "http://schemas.datacontract.org/2004/07/FinMkts.PubSub.Service");
        static readonly AsyncCallback OnNotifyCompleted = NotifyCompleted;

        readonly Subscriptions _subscriptions = new Subscriptions();
        private Subscriber _subscriber;

        // import using MEF
        [Import]
        public Subscriber Subscriber
        {
            get { return _subscriber; }
            set
            {
                if (_subscriber == value) return;
                _subscriber = value;
                if (_subscriber != null)
                {
                    // try to publish every message received
                    _subscriber.Subscribe(message => Publish(message.Topic, Encoding.UTF8.GetString(message.Body)));
                    _subscriber.Start();
                }
            }
        }

        /// <summary>
        /// Start receiving notifications for a topic
        /// </summary>
        /// <param name="topic"></param>
        public void Subscribe(string topic)
        {           
            var sessionId = OperationContext.Current.Channel.SessionId;
            var callbackChannel = OperationContext.Current.GetCallbackChannel<INotification>();
            _subscriptions.Subscribe(sessionId, topic, callbackChannel);
            OperationContext.Current.Channel.Faulted += UnsubscribeSession;
            OperationContext.Current.Channel.Closed += UnsubscribeSession;
        }

        /// <summary>
        /// Stop receiving notifications for a topic
        /// </summary>
        /// <param name="topic"></param>
        public void Unsubscribe(string topic)
        {           
            var sessionId = OperationContext.Current.Channel.SessionId;
            _subscriptions.Unsubscribe(sessionId, topic);
        }

        /// <summary>
        /// Publish some content for a topic
        /// </summary>
        public void Publish(string topic, string content)
        {
            if (topic == null) topic = "";
            var subscriptions = _subscriptions.GetSubscriptionsByTopic(topic);
            if (subscriptions.Count == 0) return;

            // serialize the message once
            var notificationData = new NotificationData { Topic = topic, Content = content };
            var buffer = MessageConverter.ToMessage(notificationData).CreateBufferedCopy(65536);

            // notify all subscribers
            foreach (var callbackChannel in subscriptions.Select(subscription => subscription.Channel))
            {
                try
                {
                    callbackChannel.BeginNotify(buffer.CreateMessage(), OnNotifyCompleted, callbackChannel);
                }
                catch (CommunicationException)
                {
                    // empty
                }
            }
        }

        static void NotifyCompleted(IAsyncResult result)
        {
            try
            {
                var notification = (INotification)(result.AsyncState);
                notification.EndNotify(result);
            }
            catch (CommunicationException)
            {
                // empty
            }
            catch (TimeoutException)
            {
                // empty
            }
        }

        void UnsubscribeSession(object sender, EventArgs e)
        {
            var channel = (IContextChannel)sender;
            if (channel == null || channel.SessionId == null) return;
            _subscriptions.UnsubscribeSession(channel.SessionId);
        }
    }
}
